{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src=\"../../img/ods_stickers.jpg\">\n",
    "## Открытый курс по машинному обучению. Сессия № 2\n",
    "### <center> Автор материала: Евгений Машкин (@emashkin)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## <center> Индивидуальный проект по анализу данных: </center>\n",
    "## <center> Идентификация пользователя по его индивидуальным особенностям набора текста на клавиатуре </center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**План исследования**\n",
    " - Описание набора данных и признаков\n",
    " - Первичный анализ признаков\n",
    " - Первичный визуальный анализ признаков\n",
    " - Закономерности, \"инсайты\", особенности данных\n",
    " - Предобработка данных\n",
    " - Создание новых признаков и описание этого процесса\n",
    " - Кросс-валидация, подбор параметров\n",
    " - Построение кривых валидации и обучения \n",
    " - Прогноз для тестовой или отложенной выборки\n",
    " - Оценка модели с описанием выбранной метрики\n",
    " - Выводы\n",
    " \n",
    " Более детальное описание [тут](https://goo.gl/cJbw7V)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "%matplotlib inline"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Задача"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как известно, каждый человек обладает уникальным почерком, и существует специальная наука - графология, которая позволяет идентифицировать автора рукописного текста или подписи в документе. Подобно рукописному тексту каждый человек обладает некоторыми особенностями цифрового ввода текста будь то набор текста на физической клавиатуре ноутбука или на виртуальной клавиатуре мобильного устройства: сила нажатия, длительность нажатия определнных символов, временные интервалы между вводом различных символов и т.д. \n",
    "\n",
    "Сегодня я попробую решить задачу определения пользователя при вводе им пароля на компьютере.\n",
    "\n",
    "Данная задача классификации может быть полезна для обеспечения дополнительной аутентицикации пользователей. Например в случае, если злоумышленник узнав пароль от вашего устройства завладеет им, то специальное ПО может определить факт использования устройства злоумышленником и заблокировать устройство, либо запросить дополнительную информацию (ответить на секретный вопрос, ввести пароль из смс/email и т.д.).\n",
    "\n",
    "Будем решать задачу не просто бинарной классификации, где бы мы выбрали одного пользователя, присвоили ему положительный класс и остальным пользователям присвоили бы отрицательный класс, а давайте представим, что у каждого пользователя есть какое-то устройство, и нам необходимо построить модель которая одинаково хорошо работала бы на вех устройствах и определяла на каждом устройстве своего владельца. Другими словами будем решать задачу многоклассовой классификации, причем для каждого класса будем решать задачу 1-vs-ALL, т.к. мы хотим отделять владельцев от остальных людей."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 1. Описание набора данных и признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В качестве данных используется датасет [Keystroke Dynamics - Benchmark Data Set](http://www.cs.cmu.edu/~keystroke/). Он содержит данные по вводу одного и того же пароля группой людей из 51 человека на ноутбуке под управлением Windows XP. Всеми испытуемыми вводился один и тот же пароль: **`.tie5Roanl`**. Данный пароль был выбран, как стойкий к взлому 10-символьный пароль. \n",
    "\n",
    "Каждый испытуемый повторял данную процедуру 400 раз в течение 8 сессий (50 вводов пароля за сессию). Если испытуемый ошибался при вводе пароля, то такая попытка не засчитывалась, и испытуемому необходимо было повторить ввод. Между каждой сессией была пауза минимум в один день. \n",
    "\n",
    "Каждая строчка в датасете - это одна попытка ввода пароля. Таким образом в датасете содержится 51 х 400 = 20400 сэмплов."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В датасете присутствуют следующие признаки:\n",
    "- признак **\"`subject`\"** - идентификатор испытуемого от s002 до s057, а не от s001 до 051. Не все испытуемые учавствовали во всех сессиях по вводу пароля.\n",
    "- признак **\"`sessionIndex\"`** - номер сессии по вводу пароля.\n",
    "- признак **\"`rep`\"** - номер повторения ввода пароля испытуемым в рамках одной сессии.\n",
    "\n",
    "- Признаки вида **\"`H.<символ>`\"** - время в секундах, в течение которого кнопка с *`<символом>`* была нажата. Например признак **\"`H.e`\"** содержит информацию о времени в течение которого была нажата кнопка *`e`*.\n",
    "- Признаки вида **\"`DD.<символ_1>.<символ_2>`\"** - количество секунд, между моментами времени, когда был нажат *`<символ_1>`* и нажат *`<символ_2>`*. Например, признак **\"`DD.i.e`\"** содержит информацию о количестве секунд между моментами времени, когда были нажаты символы *`i`* и *`e`*.\n",
    "- Признаки вида **\"`UD.<символ_1>.<символ_2>`\"** - количество секунд, между моментами времени, когда был \"отпущен\" *`<символ_1>`* и момент, когда был нажат *`<символ_2>`*. Например, признак **\"`UD.i.e`\"** содержит информацию о количестве секунд между моментами времени, когда был \"отпущен\" символ *`i`* и нажат символ *`e`*. Обратите внимание, что данная величина может быть отрицательной.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Небольшая схемка для пояснения:\n",
    "<center>\n",
    "<img src=\"../../img/Feature_explanation.jpg\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Загрузим данные:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = pd.read_csv('../../data/DSL-StrongPasswordData.csv')\n",
    "df.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Целевая переменная"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Признак **Subject** соержит ID испытуемого от s002 до s057 в строковом формате. Закодируем их в целочисленные метки классов при помощи LabelEncoder и сложим их в столбец `target`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.preprocessing import LabelEncoder\n",
    "\n",
    "le = LabelEncoder()\n",
    "df['target'] = le.fit_transform(df['subject'])\n",
    "df.drop(['subject'], axis=1, inplace=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 2. Первичный анализ признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Обработка пропущенных значений"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Проверим есть ли у нас пропущенные значения в признаках или целевой переменной:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.isnull().any().any()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Отлично, пропущенных значений нет."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Анализ признакового пространства"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Т.к. у нас 33 признака, выделим некоторые группы признаков для удобства и посмотрим на них в отдельности:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ud_features = [column for column in df.columns if 'UD' in column]\n",
    "dd_features = [column for column in df.columns if 'DD' in column]\n",
    "hold_features = [column for column in df.columns if 'H' in column]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df[ud_features].describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Можем заметить:\n",
    "- все значения лежат приблизительно в одних и тех же пределах\n",
    "- значения признаков не противоречат природе данных\n",
    "- мы можем наблюдать достаточно большие максимальные значения: 26 сек, 12 сек, 8 сек и т.д. С одной стороны это не противоречит здравому смыслу - возможны ситуации, когда, например, пользователь, отвлекся от ввода пароля на какие-то внешние факторы, но с другой стороны скорее всего это не типпичная ситуация ввода пароля и скорее всего это выбросы. Давайте более пристально посмотрим на эти значения при визуальном анализе."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Взглянем на DD признаки:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df[dd_features].describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видим, что:\n",
    "- все значения лежат приблизительно в одних и тех же пределах\n",
    "- значения признаков не противоречат природе данных\n",
    "- также, как и в предыдущей группе признаков, наблюдаем достаточно большие максимальные значения: 26 сек, 12 сек, 8 сек и т.д. Скорее всего это выбросы. Давайте более пристально посмотрим на эти значения при визуальном анализе."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df[hold_features].describe()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Аномалии в Hold-признаках отсутствуют:\n",
    "- отрицательных значений нет\n",
    "- все значения лежат приблизительно в одних и тех же пределах\n",
    "- значения признаков не противоречат природе данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 3. Первичный визуальный анализ признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Давайте построим heatmap корреляции признаков:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(14, 10))\n",
    "sns.heatmap(df[ud_features + dd_features + hold_features + ['target']].corr())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видим: \n",
    "- сильную корреляцию группы признаков DD с группой признаков UD. Оно и не удивительно т.к. признак DD является линейной комбинацией признаков UD и H: DD = UD + Hold. Одну из этих групп признаков можно выкинуть.\n",
    "- сильную корреляцию признав группы **H** между собой. Мы можем предположить, что что каждый индивид имеют свою собственную \"задержку\" нажатия на различные символы. Или другими словами он держит символ *`i`* нажатым также долго, как и символ *`t`*. Это может оказаться хорошей идеей для новых признаков.\n",
    "- некоторую корреляцию признав группы **UD** (соответственно и группы DD) между собой. Мы можем предположить, что что каждый индивид имеют свою собственную скорость набора пароля. Но эта корреляция не столь сильна, возможно потому, что  некоторые пары символов расположены рядом и для их набора используется одна рука, для других - две руки (в этом случае их набор, скорее всего, будет быстрее). Это может оказаться хорошей идеей для новых признаков."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь давайте взглянем распределения признаков и их взаимные зависимости. Т.к. признаков много построим pairplot-ы отдельно для групп признаков UD и Hold:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.pairplot(df[hold_features + ['target']], hue='target')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.pairplot(df[ud_features + ['target']], hue='target', palette=sns.color_palette(\"hls\", 51))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Из графиков выше видим:\n",
    "- на графиках отчетливо видно, что, да, мы имеем небольшое число выбросов\n",
    "- распределения признаков UD кажутся сильно скошенными, но это также может быть следствием наличия выбросов и малого количества столбцов в гистограмах. Взглянем еще раз на распределения после удаления выбросов"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Распределение целевой переменной"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Классы в датасете сбалансированы и имеют равное количество объектов (по 400 шт.), что обусловлено сбором данных:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(12,6))\n",
    "sns.countplot(x='target', data=df)\n",
    "plt.xlabel('Пользователь')\n",
    "plt.ylabel('Кол-во объектов в классе')\n",
    "plt.title('Распределение целевой переменной')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### PCA Анализ"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Давайте проанализируем данные при помощи метода главных компонент. Для этого предварительно отмасштабируем данные."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.decomposition import PCA\n",
    "from sklearn.preprocessing import StandardScaler\n",
    "\n",
    "X = df.drop(['target'], axis=1)\n",
    "y = df['target'].values\n",
    "scaler = StandardScaler()\n",
    "X_scaled = scaler.fit_transform(X)\n",
    "\n",
    "pca = PCA(random_state=17)\n",
    "pca.fit(X_scaled)\n",
    "\n",
    "pca_result = pca.transform(X_scaled)\n",
    "pca_result.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь взглянем на зависимость количества объясненной информации от количества галвных компонент:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(10,5))\n",
    "plt.plot(np.cumsum(pca.explained_variance_ratio_)*100)\n",
    "plt.xlabel('Количество главных компонент')\n",
    "plt.ylabel('% Объясненной информации в данных')\n",
    "plt.title('Зависимость объясненной информации от количества главных компонент')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Из графика выше видим, что:\n",
    "- часть компонент не привносит никакой новой информации и являются избыточными - мы можем снизить размерность данных до 23. Это можно объяснить тем, что часть признаков избыточны, что также видно из матрицы корреляций признаков.\n",
    "- информация в датасете распределена более-менее равномерно среди остальных признаков - мы не можем существенно уменьшить размерность данных (скажем до 3-5 признаков) без потери ощутимой части информации в данных."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Также взглянем на проекцию данных на первые две главные компоненты:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(12,8))\n",
    "plt.scatter(pca_result[:,0], pca_result[:,1], c=df['target'], s=2, cmap='viridis_r', alpha=0.4)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видим, что в целом объекты одного класса кучкуются в одном месте, но объекты разных классов достаточно трудно разделимы друг от друга. Это может свидетельствовать о том, что:\n",
    "- либо объекты разных классов в принципе могут быть разделимы, но первые две компоненты не содержат достаточно информации, чтобы это сделать\n",
    "- либо объекты разных классов в принципе могут быть разделимы, но необходимо применить какие-то другие нелинейные методы преобразования данных, чтобы можно было осуществить четкое разделение классов\n",
    "- либо объекты разных классов в принципе не разделимы"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Анализ данных при помощи t-SNE"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Давайте проверим вторую гипотезу и воспользуемся нелинейными методами, например, применив t-SNE к данным и взглянем на них в 2D:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.manifold import TSNE"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "tsne = TSNE(random_state=23)\n",
    "tsne_representation = tsne.fit_transform(X_scaled, y=df['target'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(12,8))\n",
    "plt.scatter(tsne_representation[:,0], tsne_representation[:,1], c=df['target'], s=5, cmap='viridis_r', alpha=.8)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В целом видим хорошо различимые кластеры объектов, только лишь в самом центре объекты разных классов перемешаны между собой. Можно сделать вывод, что на имеющихся данных объекты могут быть достаточно хорошо разделены на классы."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 4. Закономерности, \"инсайты\", особенности данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Зависимость признаков от номера повторения ввода пароля"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Давайте проверим первую гипотезу: есть зависимость признаков от номера повторения ввода пароля. Давайте взглянем на продолжительность нажатия различных символов:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.groupby(['rep'])[hold_features].agg(np.mean).plot()\n",
    "plt.xlabel('Повторение')\n",
    "plt.ylabel('Сек')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видим легкий восходящий тренд. Это можно объяснить небольшой усталостью испытуемых - на более поздних итерациях ввода пальцы больше \"отдыхали\" на кнопках клавиатуры. И теперь взглянем на интервалы между нажатием пар символов:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.groupby(['rep'])[ud_features].agg(np.mean).plot()\n",
    "plt.xlabel('Повторение')\n",
    "plt.ylabel('Сек')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видим, что в течение первых 5-10 повторений интервалы между нажатиями соседних символов уменьшаются и потом практически не изменяются. Это можно объяснить тем, что после нескольких попыток ввода испытуемые размялись и \"запомнили\" расположения символов и движения по вводу пароля."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "С одной стороны данные зависимости характерны только для обучающих данных и обусловлены особенностью сбора данных. Но с другой стороны ситуация, когда пользователь делает несколько последовательных попыток ввода и несколько раз ошибается, также возможна на практике и модель может использовать эти данные для коррекции прогноза. Поэтому оставляем данный признак в датасете."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Зависимость признаков от номера сессии сбора данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Взглянем на зависимость признаков от номера сессии по сбору данных:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.groupby(['sessionIndex'])[hold_features].agg(np.mean).plot()\n",
    "plt.xlabel('Номер сессии')\n",
    "plt.ylabel('Сек')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.groupby(['sessionIndex'])[ud_features].agg(np.mean).plot()\n",
    "plt.xlabel('Номер сессии')\n",
    "plt.ylabel('Сек')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видим, что за несколько сессий средние интервалы между нажатием двух соседних символов уменьшилось, испытуемые в какой-то степени \"выучили\" пароль."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Не смотря на присутствие некоторых зависимостей мы не можем учитывать данный признак для решения практической задачи, т.к. его наличие обсуловлено спецификой сбора данных - во избежание сильной переутомляемости испытуемых. А также в реальной ситуации у нас не будет информации ни о каких сессиях. Убираем данный признак из датасета:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df.drop(['sessionIndex'], axis=1, inplace=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Часть 5. Выбор метрики"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Т.к. для нас важно выделять каждого пользователя среди остальных - владелец у устройства один, а потенциальных злоумышленников много, то мы будем решать 51 задачу (по количеству классов) классификации 1-vs-All. В каждой такой задаче будет сильный дисбаланс классов - 1:50, поэтому будем использовать метрику ROC_AUC, т.к. она устойчива к дисбалансу классов. \n",
    "\n",
    "Чтобы обобщить метрику для всех пользователей и оптимизировать модель для всех классов, возьмем ROC AUC с макро усреднением по классам (оно дает более пессимистичный прогноз, чем микро усреднение). "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Определим свой объект scorer, который будет бинаризировать вектор правильных ответов и считать по ним ROC AUC с макро усреднением. Далее будем передавать этот объект scorer в Grid search как метрику качетсва модели на валидации."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.metrics import make_scorer, roc_auc_score\n",
    "from sklearn.preprocessing import LabelBinarizer\n",
    "\n",
    "def custom_multi_roc_auc_score(y_true, y_pred, avg='macro'):\n",
    "\n",
    "    lb = LabelBinarizer()\n",
    "    return roc_auc_score(lb.fit_transform(y_true), y_pred, average=avg)\n",
    "\n",
    "multi_roc_auc_scorer = make_scorer(custom_multi_roc_auc_score, needs_proba=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 6. Выбор модели"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Для решения задачи выберем алгоритм из библиотеки sklearn. В ней реализовано огромное количестово методов, которые подходят для решения большинства задач.\n",
    "\n",
    "Практически все классификаторы могут решать задачу 1-vs-ALL, рассмотрим наиболее известные:\n",
    "- `KNeighborsClassifier` - метод ближайших соседей, интуитивно кажется что может сработать лучше остальных, т.к. судя по результатм визуализации t-SNE есть области признакового пространства, где объекты перемешаны достаточно сильно и метод ближайших соседей может показать себя достаточно хорошо. Возьмем данную модель для рассмотрения.\n",
    "- `DecisionTreeClassifier` - обычное дерево решений - работает очень быстро, что не так уж важно на наших данных. Обладает хорошей интерпретабельностью, что в нашей задаче также не важно. Но более сложное в подборе гиперпараметров, чем случайный лес. Между деревом решений и случайным лесом в данной задаче отдаем предпотение лесу. Данный классификатор не рассматриваем.\n",
    "- `RandomForestClassifier` - композиция деревьев решений. Обладает хорошими обобщающими способностями и достаточно прост в настройке гиперпараметров для получения хорошего результата. Также может воспроизводить не линейные зависимости. Возьмем данную модель для рассмотрения.\n",
    "- `LogisticRegression` - линейная модель, которая может предсказывать вероятности отнесения к классам. Недостатком в данной задаче может быть то, что без дополнительных трюков (например спрямляющих пространств) модель может только строить линейные зависимости.\n",
    "- `RidgeClassifier` - обычный линейный классификатор с L2 регуляризацией. Не может предсказывать вероятности отнесения к классам, что необходимо для нашей выбранной метрики ROC AUC. Поэтому данный классификатор не рассматриваем.\n",
    "- `SGDClassifier` - хорошо работает на больших объемах данных за счет SGD. У нас данных не много и другие линейные классификаторы скорее всего покажут чуть более высокие значения метрики качества. Поэтому данный классификатор не рассмариваем.\n",
    "\n",
    "Резюмируя: судя по результатам визуализации t-SNE, есть области признакового пространства, где объекты разных классов сильно перемешаны и линейной модели будет трудно их разделить, или по крайней мере t-SNE не смог эти зависимости \"распрямить\" и сделать кластеры хорошо отделимыми друг от друга (но нужно помнить, что t-SNE не дает 100% результат). На основании этого кажется, что линейные модели справятся хуже всего на этой задаче без введения дополнительных признаков, а метод ближайших соседей и случайный лес должны показать лучший результат. Далее будем рассматривать:\n",
    "- `KNeighborsClassifier`\n",
    "- `RandomForestClassifier`\n",
    "- `LogisticRegression`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 7. Предобработка данных "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Обработка пропусков в данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как мы уже увидели ранее, пропусков в данных нет."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Обработка выбросов"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Линейные модели и методоб ближайших соседей чувствительны к масштабу данных и выбросам в данных. Поэтому удалим объекты с выбросами и отмасштабируем данные."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Уберем выбросы \"в ручную\" - их не так много и их достачно легко определить на графиках распределения признаков."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df = df[(df['H.o']<=0.5)&(df['H.a']<=0.5)&(df['UD.period.t']<=5)&(df['UD.t.i']<=5)&(df['UD.i.e']<=5)]\n",
    "df.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Таким образом мы убрали 7 выбросов. Посмотрим на распределение признаков еще раз:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.pairplot(df[hold_features + ['target']], hue='target', palette=sns.color_palette(\"hls\", 51))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sns.pairplot(df[ud_features + ['target']], hue='target', palette=sns.color_palette(\"hls\", 51))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Разбиваем данные на две части для обучения и валидации"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Разделяем данные на признаки и прогнозируемые метки классов:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X = df[ud_features + hold_features + ['rep']]\n",
    "y = df['target']\n",
    "X.shape, y.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Разбиваем данные на обучающую и отложенную выборки. В отложенную выборку возьмем 30% данных. Разбиение выполняем стратифицированно с перемешиванием - чтобы отношение всех классов в обучающей и отложенной выборке было одинаковым и не получилось ситуации, при которой в отложенной выборке присутствуют классы, котрые мы не видели на обучении."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, test_size=0.3, random_state=19, shuffle=True)\n",
    "X_train.shape, X_test.shape, y_train.shape, y_test.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Масштабирование данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Масштабируем данные для линейной модели, для этого создаем объект скейлер и обучаем его на тренировочной части. И применяем полученное преобразование к обоим выборкам:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.preprocessing import StandardScaler\n",
    "\n",
    "scaler = StandardScaler()\n",
    "X_train_scaled = scaler.fit_transform(X_train)\n",
    "X_test_scaled = scaler.transform(X_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 8. Кросс-валидация, подбор параметров"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Будем выполнять стратифицированную кросс-валидацию на 4 фолдах:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import GridSearchCV, StratifiedKFold\n",
    "\n",
    "strtf_cv = StratifiedKFold(n_splits=4, random_state=17)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Сразу определим полезную функцию, которая будет строит зависимость результата на обучении и на валидации при варьировании одного гиперпараметра модели. Варьируемый гиперпараметер передается в функцию параметром:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_cv_results(grid, param='param_n_neighbors', scale='linear'):\n",
    "    '''\n",
    "    This function plots gridsearch results for given grid search object. It can only plot \n",
    "    results for one parameter (default parameter is C).\n",
    "    '''\n",
    "    plt.figure(figsize=(10,6))\n",
    "    plt.plot(grid.cv_results_[param].tolist(), grid.cv_results_['mean_train_score'], label='Train Score')\n",
    "    plt.fill_between(grid.cv_results_[param].tolist(), \n",
    "                     grid.cv_results_['mean_train_score'] - grid.cv_results_['std_train_score']/2, \n",
    "                     grid.cv_results_['mean_train_score'] + grid.cv_results_['std_train_score']/2, \n",
    "                     alpha=.3)\n",
    "    plt.plot(grid.cv_results_[param].tolist(), grid.cv_results_['mean_test_score'], label='Validation Score')\n",
    "    plt.fill_between(grid.cv_results_[param].tolist(), \n",
    "                     grid.cv_results_['mean_test_score'] - grid.cv_results_['std_test_score']/2, \n",
    "                     grid.cv_results_['mean_test_score'] + grid.cv_results_['std_test_score']/2, \n",
    "                     alpha=.3)\n",
    "    plt.ylabel('ROC AUC')\n",
    "    plt.xlabel(param.strip('param_'))\n",
    "    plt.title('Validation Curve')\n",
    "    plt.legend()\n",
    "    plt.xscale(scale)\n",
    "    plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### KNN"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В данной задаче метод ближайших соседей должен показать себя хорошо. Сначала попробуем метрику минковского. При подборе параметров будем варьировать гиперпараметры:\n",
    "- n_neighbors - количество ближайших соседий, по которым выполняется усреднение. Количество ближайших соседей - отвечает за сложность модели. Меньшие значения n_neighbors соответствуют более сложной модели.\n",
    "- p - степенной параметр метрики Минковского. р=2 - классическое Евклидово расстояние, р=1 - Манхеттанская метрика расстояния (сумма растояний между объектами вдоль осей координат). Подбираем эмпирически.\n",
    "\n",
    "Для наглядности выполним отдельно кросс-валидацию для р=1 и р=2, варьируя количество ближайших соседей."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В качестве метрики качества передаем наш объект scorer, определенный ранее."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.neighbors import KNeighborsClassifier\n",
    "\n",
    "knn_clf = KNeighborsClassifier(n_jobs=-1)\n",
    "\n",
    "knn_params = {\n",
    "    'n_neighbors': list(np.arange(1, 10)) + list(np.linspace(10, 150, 15, dtype=int))\n",
    "}\n",
    "\n",
    "knn_grid = GridSearchCV(knn_clf, param_grid=knn_params, n_jobs=-1, scoring=multi_roc_auc_scorer, cv=strtf_cv)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "knn_grid.fit(X_train_scaled, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('Лучшее значение ROC AUC:', knn_grid.best_score_)\n",
    "print('Лучшие параметры:', knn_grid.best_params_)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Давайте построим кривые обучения и валидации:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_cv_results(knn_grid, param='param_n_neighbors')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видим ожидаемый результат: ROC AUC на обучении уменьшается от 1 с ростом числа ближайших соседей. ROC AUC на валидации на против - растет до некоторого уровня и затем практически перестает расти и не меняется. Также видим, что разброса значений метрики качества ни на обучении, ни на валидации практически нет."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "И давайте провери качество модели (ROC AUC) на отложенных данных, которые модель не видела. Также используем ROC AUC с макро усреднением:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "custom_multi_roc_auc_score(y_test, knn_grid.best_estimator_.predict_proba(X_test_scaled))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Результат на отложенной выборке даже немного выше, чем на валидации, что говорит о том, что модель не переобучена и гиперпараметр подобраны правильно."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь давайте изменим метрику расстояния на Манхеттанскую:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "knn_clf = KNeighborsClassifier(p=1, n_jobs=-1)\n",
    "\n",
    "knn_params = {\n",
    "    'n_neighbors': list(np.arange(1, 10)) + list(np.linspace(10, 150, 15, dtype=int))   \n",
    "}\n",
    "\n",
    "knn_grid_manhattan = GridSearchCV(knn_clf, param_grid=knn_params, n_jobs=-1, scoring=multi_roc_auc_scorer, cv=strtf_cv)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "knn_grid_manhattan.fit(X_train_scaled, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('Лучшее значение ROC AUC:', knn_grid_manhattan.best_score_)\n",
    "print('Лучшие параметры:', knn_grid_manhattan.best_params_)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_cv_results(knn_grid, param='param_n_neighbors')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видим такой же характер кривых на графике, что и в предыдущем случае, но лучший результат на валидации оказался лучше -  0.9969 против 0.9893."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Проверим качество на отложенной выборке:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "custom_multi_roc_auc_score(y_test, knn_grid_manhattan.best_estimator_.predict_proba(X_test_scaled))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Качество на отложенной выборке получилось еще выше."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Здесь необходимо сделать отступление - может показаться, что у нас получились практически идеальные классификаторы и второй лишь незначительно лучше первого. Но здесь нужно быть осторожным, ROC AUC может быть обманчивой метрикой при суждении об идеальности классификатора по близости ROC AUC к 1 в случае с сильным дисбалансом классов.\n",
    "\n",
    "Это происходит потому что False Positive Rate нормируется на величину отрицательного класса, который в нашем случае в 50 раз больше положительного класса. В этом случае кривая ROC AUC будет приближаться к точке (0,1). Другими словами в случае сильного дисбаланса класса метрика ROC AUC годится для сравнения между собой различных классификаторов и выбора лучшего из них."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Random  Forest"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Давайте теперь посмотрим на случайный лес. Будем варьировать количество деревьев в ансамбле. Чем больше деревьев в ансамбле, тем более сглаженной будет граница разбиения классов."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.ensemble import RandomForestClassifier\n",
    "rf_clf = RandomForestClassifier(random_state=17)\n",
    "\n",
    "rf_params = {\n",
    "    'n_estimators': np.arange(15, 415, 15)\n",
    "}\n",
    "rf_grid = GridSearchCV(rf_clf, param_grid=rf_params, n_jobs=-1, scoring=multi_roc_auc_scorer, cv=strtf_cv)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "rf_grid.fit(X_train_scaled, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('Лучшее значение ROC AUC:', rf_grid.best_score_)\n",
    "print('Лучшие параметры:', rf_grid.best_params_)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_cv_results(rf_grid, param='param_n_estimators')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Здесь также видим закономерную ситуацию для случайного леса: ошибки на обучении нет, также с ростом количества деревьев в ансамбле качество на валидации улучшается. Кроме того при некоторых начальных n_estimators видим небольшой разброс значений ROC AUC на кросс валидации (больше, чем у kNN). Но при дальнейшем увеличении числа деревьев разброса практически нет."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "custom_multi_roc_auc_score(y_test, rf_grid.best_estimator_.predict_proba(X_test_scaled))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Значение на отложенной выборке чуть выше, чем на валидации, что также говорит о том, что мы не переобучились и правильно подобрали гиперпараметры."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Logistic Regression"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь посмотрим на логистическую регрессию. Основной гиперпараметр - С величина обратная коэффициенту регуляризации. Чем меньше С, тем более сложная модель и более сложные зависимости она может выучивать."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.linear_model import LogisticRegression"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lr_clf = LogisticRegression(random_state=17)\n",
    "\n",
    "lr_params = {\n",
    "    'C': np.logspace(-3,2,25) \n",
    "}\n",
    "lr_grid = GridSearchCV(lr_clf, param_grid=lr_params, n_jobs=1, scoring=multi_roc_auc_scorer, cv=strtf_cv)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "lr_grid.fit(X_train_scaled, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('Лучшее значение ROC AUC:', lr_grid.best_score_)\n",
    "print('Лучшие параметры:', lr_grid.best_params_)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_cv_results(lr_grid, param='param_C', scale='log')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видим что значение ROC AUC на валидации возрастало до некоторого значения С, после него ROC AUC стала немного уменьшаться. Также устойчивость модели логистической регрессии получилась хуже, чем у kNN и RandomForest. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "custom_multi_roc_auc_score(y_test, lr_grid.best_estimator_.predict_proba(X_test_scaled))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Логистическая модель показала себя хуже, чем другие модели."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### ROC кривые"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Давайте построим все три ROC кривые для моделей с лучшими параметрами, а также построим ROC кривую для постоянного прогноза негативного класса (если бы наша модель все время предсказывала 0):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.metrics import roc_curve\n",
    "lb = LabelBinarizer()\n",
    "fpr = dict()\n",
    "tpr = dict()\n",
    "\n",
    "fpr['knn'], tpr['knn'], _ = roc_curve(lb.fit_transform(y_test).ravel(), \\\n",
    "                                          knn_grid_manhattan.best_estimator_.predict_proba(X_test_scaled).ravel())\n",
    "fpr['rf'], tpr['rf'], _ = roc_curve(lb.fit_transform(y_test).ravel(), \\\n",
    "                                          rf_grid.best_estimator_.predict_proba(X_test_scaled).ravel())\n",
    "fpr['lr'], tpr['lr'], _ = roc_curve(lb.fit_transform(y_test).ravel(), \\\n",
    "                                          lr_grid.best_estimator_.predict_proba(X_test_scaled).ravel())\n",
    "fpr['all_zeros'], tpr['all_zeros'], _ = roc_curve(lb.fit_transform(y_test).ravel(), \\\n",
    "                                          np.zeros_like(lb.fit_transform(y_test)).ravel())\n",
    "\n",
    "plt.figure(figsize=(12,8))\n",
    "plt.plot(fpr['knn'], tpr['knn'], color='darkorange', lw=1, label='KNN ROC')\n",
    "plt.plot(fpr['rf'], tpr['rf'], color='darkgreen', lw=1, label='Random Forest ROC')\n",
    "plt.plot(fpr['lr'], tpr['lr'], color='darkred', lw=1, label='Logistic Regression ROC')\n",
    "plt.plot([0, 1], [0, 1], color='black', linestyle='--', lw=1, label='All Zeros')\n",
    "plt.xlabel('False Positive Rate')\n",
    "plt.ylabel('True Positive Rate')\n",
    "plt.legend()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Видим, что при сильном дисбалансе классов ROC кривые сильно прижимаются к точке (0,1). Но все равно мы можем сделать вывод, что случайный лес показал себя лучше других моделей. Дальнейшие исследования продолжим со случайным лесом."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 9. Построение кривой обучения "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь посмотрим на зависимость качества модели от количества данных на обучении. Т.к. мы уже подобрали оптимальные параметры модели на кросс валидации, то мы можем использовать все доступные данные для построения кривой обучения:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import learning_curve"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "train_sizes, train_scores, valid_scores = learning_curve(rf_grid.best_estimator_, X, y, scoring=multi_roc_auc_scorer,\n",
    "                                                         train_sizes=np.linspace(0.05, 1.0, 20), cv=strtf_cv, n_jobs=-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(10,6))\n",
    "plt.plot(train_sizes, np.mean(train_scores, axis=1), label='Train Score')\n",
    "plt.fill_between(train_sizes, \n",
    "                 np.mean(train_scores, axis=1) - np.std(train_scores, axis=1)/2, \n",
    "                 np.mean(train_scores, axis=1) + np.std(train_scores, axis=1)/2, \n",
    "                 alpha=.3)\n",
    "plt.plot(train_sizes, np.mean(valid_scores, axis=1), label='Validation Score')\n",
    "plt.fill_between(train_sizes, \n",
    "                 np.mean(valid_scores, axis=1) - np.std(valid_scores, axis=1)/2, \n",
    "                 np.mean(valid_scores, axis=1) + np.std(valid_scores, axis=1)/2, \n",
    "                 alpha=.3)\n",
    "plt.ylabel('ROC AUC')\n",
    "plt.xlabel('Количество данных')\n",
    "plt.title('Learning Curve')\n",
    "plt.legend()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Можем сделать вывод, что:\n",
    "- возможно при добавлении новых данных можно было бы еще немного улучшить качество модели\n",
    "- либо попробовать добавить еще новых данных с последующим усложнением модели"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 10. Создание новых признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Давайте попробуем ввести новые признаки, которые характеризовали бы каждую попытку ввода пароля пользователем в целом например по усредненным параметрам:\n",
    "- **`avg_h`** - среднее время нажатия одного символа в течение одной попытки ввода пароля, т.е. среднее всех H-признаков в наблюдении\n",
    "- **`avg_ud`** - среднее время между нажатием двух соседних символов в пароле в течение одной попытки ввода пароля, т.е. среднее всех UD-признаков. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "df['avg_h'] = df[hold_features].apply(np.mean, axis=1)\n",
    "df['avg_ud'] = df[ud_features].apply(np.mean, axis=1)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Еще раз обучим модель на кросс валидации:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X = df[ud_features + hold_features + ['rep', 'avg_h', 'avg_ud']]\n",
    "y = df['target']\n",
    "X.shape, y.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "И занового разбиваем данные на обучение и тест:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, test_size=0.3, random_state=19, shuffle=True)\n",
    "X_train.shape, X_test.shape, y_train.shape, y_test.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "rf_clf = RandomForestClassifier(random_state=17)\n",
    "\n",
    "rf_params = {\n",
    "    'n_estimators': np.arange(15, 415, 15)\n",
    "}\n",
    "rf_grid_new_features = GridSearchCV(rf_clf, param_grid=rf_params, n_jobs=-1, scoring=multi_roc_auc_scorer, cv=strtf_cv)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "rf_grid_new_features.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('Лучшее значение ROC AUC:', rf_grid.best_score_)\n",
    "print('Лучшие параметры:', rf_grid.best_params_)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "На кросс-валидации мы получили абсолютно идентичное значение ROC AUC как и без новых признаков. Давайте посмотрим на значение метрики на отложенной выборке:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "custom_multi_roc_auc_score(y_test, rf_grid_new_features.best_estimator_.predict_proba(X_test))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Значение ROC AUC на отложенной выборке уменьшилось на 0.0001. Можем сделать вывод, что после добавления новых признаков качество модели не улучшилось. Убираем новые признаки из модели:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X = df[ud_features + hold_features + ['rep']]\n",
    "y = df['target']\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, test_size=0.3, random_state=19, shuffle=True)\n",
    "X_train.shape, X_test.shape, y_train.shape, y_test.shape\n",
    "X.shape, y.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Часть 11. Оценка модели с описанием выбранной метрики"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Что ж, мы подобрали лучшие параметры модели по ROC AUC, чтобы она разделяла наши классы луше всего. Попробуем теперь понять на сколько хорошую модель мы получили и перевести это в понятную бизнес метрику. \n",
    "\n",
    "Возьмем лучший эстиматор полученный в результате подбора параметров на кросс-валидации без новых признаков и обучим его на всех тренировочных данных:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "best_model = rf_grid.best_estimator_\n",
    "best_model.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь посмотрим в скольких случаях в среднем ошибается модель на новых данных, которые она не видела. Для этого воспользуемся метрикой accuracy и будем сравнивать ее с моделью, которая всегда предсказывает больший класс - негативный. \n",
    "\n",
    "Для такой модели accuracy составляет 50 / 51 = 0.9803."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Чтобы подобрать оптимальное разбиение, будем варьировать `t` - порог предсказанной вероятности отнесения пользователя к положительному классу, т.е. к владельцу устройства, и усреднять получе точность (accuracy) модели по всем классам:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "lb = LabelBinarizer()\n",
    "\n",
    "acc = []\n",
    "y_test_binarized = lb.fit_transform(y_test)\n",
    "y_predicted = best_model.predict_proba(X_test)\n",
    "ts = np.linspace(0.005, 1.0, 200)\n",
    "for t in ts:\n",
    "    local_acc = []\n",
    "    for i in range(51):\n",
    "        local_acc.append(accuracy_score(y_test_binarized[:,i], y_predicted[:,i]>=t))\n",
    "    acc.append(local_acc)\n",
    "\n",
    "plt.figure(figsize=(10,6))\n",
    "plt.plot(ts, np.mean(acc, axis=1), color='g', label='Лучшая модель')\n",
    "plt.hlines(y=0.98, xmin=0, xmax=1, lw=1, label='All-zero')\n",
    "plt.xlabel('t, Порог предсказанной вероятности')\n",
    "plt.ylabel('Accuracy')\n",
    "plt.legend()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Отлично, видим, что при определенных значениях `t` модель делает предсказания лучше \"пессимистичных\" и точность модель близка к единице. Давайте посмотрим на точные цифры:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "max_accuracy = max(np.mean(acc, axis=1))\n",
    "best_t = ts[np.array(max_accuracy).argmax()]\n",
    "\n",
    "print('Лучшее значение точности {} при t={}'.format(max_accuracy.round(5), best_t))\n",
    "print('В среднем модель ошиблась в {} случаях из {}, в которых было {} объектов положительного класса'\\\n",
    "              .format(round((1-max_accuracy)*X_test.shape[0]), \n",
    "                      X_test.shape[0], \n",
    "                      round((y_test==10).mean() * y_test.shape[0])))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Часть 11. Выводы "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Мы увидели, что действительно каждый человек обладает собственным \"цифровым подчерком\" при вводе информации на компьютере. Построенная нами даже относительно простая модель работает значительно лучше постоянного пессимистичного классификатора. Но скорее всего полученная точность модели пока не позволяет использовать ее в качестве самостоятельной системы аутентификации пользователей, а только лишь, как дополняющую другие способы.\n",
    "\n",
    "Качество прогнозов полученной модели можно попробовать улучшить за счет:\n",
    "- более глубокого тюнинга гиперпараметров\n",
    "- дополнительных данных для обучения модели\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
